Skip to content

Re-run dep-scan when its parent compile is affected#51

Merged
typeless merged 1 commit into
mainfrom
fix/dep-scan-self-edges
May 15, 2026
Merged

Re-run dep-scan when its parent compile is affected#51
typeless merged 1 commit into
mainfrom
fix/dep-scan-self-edges

Conversation

@typeless

Copy link
Copy Markdown
Owner

Summary

Fixes the transitive implicit-dep tracking bug reported in pup-header-detection-bug.md (2026-05-15 update) and reproduced by the [\!shouldfail]-tagged SCENARIO landed in d71e75b. This PR makes that SCENARIO turn green and converts it into a permanent regression guard.

The bug

When an already-tracked header is edited to #include a new header (or any change introduces a new transitive dependency), the parent compile re-runs correctly, but the sibling dep-scan command (OutputAction::InjectImplicitDeps) does not. The dep-scan therefore never sees the new transitive header, never reports it as a discovered dep, and the resulting implicit-dep edge is never recorded in the index. Subsequent edits to the new transitive header then appear invisible to change detection — pup reports Nothing to do with a stale .o.

Root cause

collect_affected_commands walks the build graph forward from changed files via get_outputs (consumers) and get_order_only_dependents. Dep-scan commands capture stdout rather than producing file outputs, so they have zero graph outputs. The existing cascade therefore cannot reach them, and they never get added to the affected set — no matter how many of their implicit deps changed.

Diagnosed with pup show index against the reproducer fixture:

c2147483649  [src]
     cmd: gcc -I../include -c main.c -o ../build/x86/src/main.o
     implicit: include/old.h           ← compile records its deps
c2147483650  [src]
     cmd: gcc -M -I../include main.c
     sticky:   src/Tupfile             ← dep-scan has no implicit deps;
                                          can't be marked dirty on header change

Fix

In collect_affected_commands, after the file-cascade walk, attach any command whose parent_command is in the affected set. The graph already tracks the parent-command linkage on dep-scan nodes (set in dep_scanner.cpp and rule_pattern.cpp); we just leverage it to bind the dep-scan's dirty status to its parent's.

for (auto cmd_id : nodes_of_type(graph, NodeType::Command)) {
    auto parent = get_parent_command(graph, cmd_id);
    if (parent \!= INVALID_NODE_ID && affected.contains(parent)) {
        affected.set(cmd_id, 1);
    }
}

12 lines in dag.cpp plus removing the [\!shouldfail] tag and the CHECK-instead-of-REQUIRE workaround in the regression test.

Test plan

  • Reproducer (test/e2e/fixtures/header_dep_transitive/test.sh) now exits 0 with output: PASS: pup detected transitive header change.
  • Per-step forensic from pup show index: after step 2, main.c's implicit-dep set now includes include/newhdr.h (was missing pre-fix)
  • Catch2 SCENARIO "Transitive implicit-dep header tracking" passes cleanly (no longer [\!shouldfail])
  • Full test suite: 463 cases, 23,645 assertions, all pass
  • make format clean

Scope note

This fix addresses the "edit existing header to introduce new transitive include" scenario from pup-header-detection-bug.md. The related scenario in docs/bug-unscoped-build-missing-new-header-dep.md (brand-new src + brand-new header in same build) may be a distinct first-build code path; not addressed here. The current PR is the minimal fix for the reproducer that exists.

🤖 Generated with Claude Code

When an already-tracked header is edited to add a #include of a new header (or any change that introduces a new transitive dependency), pup must re-run not only the parent compile but also its sibling dep-scan command (OutputAction::InjectImplicitDeps). Otherwise the dep-scan never sees the new transitive header, never reports it as a discovered dep, and the resulting implicit-dep edge is never recorded in the index. Subsequent edits to the new transitive header then appear invisible to change detection and pup reports "Nothing to do" with a stale .o.

Fix: in collect_affected_commands, after the file-cascade walk, attach any command whose parent_command is in the affected set. Dep-scan commands have no graph outputs (they capture stdout, not files), so the existing cascade cannot reach them — they must be tied to their parent explicitly.

Converts the [\!shouldfail] reproducer added in d71e75b into a passing regression guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@typeless typeless force-pushed the fix/dep-scan-self-edges branch from 7103271 to 608d851 Compare May 15, 2026 07:22
@typeless typeless merged commit 79debce into main May 15, 2026
9 checks passed
@typeless typeless deleted the fix/dep-scan-self-edges branch May 15, 2026 07:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant